package dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import model.Conta;

public final class ContaDAO {

    private static final String SQL_FIND_BY_NRO_CONTA =
        "SELECT nro_conta, nome_cliente, senha, saldo FROM conta WHERE nro_conta = ?";
    private static final String SQL_FIND_BY_NOMECLIENTE_E_SENHA =
        "SELECT nro_conta, nome_cliente, senha, saldo FROM conta WHERE nome_cliente = ? AND senha = ?";
    private static final String SQL_LIST_ORDER_BY_NROCONTA =
        "SELECT nro_conta, nome_cliente, senha, saldo FROM conta ORDER BY nro_conta";
    private static final String SQL_INSERT =
        "INSERT INTO conta (nome_cliente, senha, saldo) VALUES (?, ?, ?)";
    private static final String SQL_UPDATE =
        "UPDATE conta SET nome_cliente = ?, senha = ?, saldo = ? WHERE nro_conta = ?";
    private static final String SQL_DELETE =
        "DELETE FROM conta WHERE nro_conta = ?";
    private static final String SQL_EXIST_NOMECLIENTE =
        "SELECT nro_conta FROM conta WHERE nome_cliente = ?";


    private DAOFactory daoFactory;

    ContaDAO(DAOFactory daoFactory) {
        this.daoFactory = daoFactory;
    }

    public Conta find(Long nroConta) throws DAOException {
        return find(SQL_FIND_BY_NRO_CONTA, nroConta);
    }

    public Conta find(String nomeCliente, String senha) throws DAOException {
        return find(SQL_FIND_BY_NOMECLIENTE_E_SENHA, nomeCliente, DAOUtil.hashMD5(senha));
    }

    private Conta find(String sql, Object... values) throws DAOException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Conta conta = null;

        try {
            connection = daoFactory.getConnection();
            preparedStatement = DAOUtil.prepareStatement(connection, sql, false, values);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                conta = mapConta(resultSet);
            }
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(resultSet);
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }

        return conta;
    }

    public List<Conta> list() throws DAOException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Conta> contas = new ArrayList<Conta>();

        try {
            connection = daoFactory.getConnection();
            preparedStatement = connection.prepareStatement(SQL_LIST_ORDER_BY_NROCONTA);
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                contas.add(mapConta(resultSet));
            }
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(resultSet);
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }

        return contas;
    }

    public void create(Conta conta) throws IllegalArgumentException, DAOException {
        if (conta.getNroConta() != null) {
            throw new IllegalArgumentException("Conta já foi criada, nro conta não é null.");
        }
        Object[] values = {
            conta.getNomeCliente(),
            hashMD5IfNecessary(conta.getSenha()),
            conta.getSaldo()
        };
        
        System.out.println(conta.getSaldo());

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet generatedKeys = null;

        try {
            connection = daoFactory.getConnection();
            preparedStatement = DAOUtil.prepareStatement(connection, SQL_INSERT, true, values);
            int affectedRows = preparedStatement.executeUpdate();
            if (affectedRows == 0) {
                throw new DAOException("Criação da conta falhou, nenhuma linha modificada.");
            }
            generatedKeys = preparedStatement.getGeneratedKeys();
            if (generatedKeys.next()) {
                conta.setNroConta(Long.valueOf(generatedKeys.getLong(1)));
            } else {
                throw new DAOException("Criação da conta falhou, não foi possível obter um nro de conta único.");
            }
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(generatedKeys);
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }
    }

    public void update(Conta conta) throws DAOException {
        if (conta.getNroConta() == null) {
            throw new IllegalArgumentException("Conta ainda não foi criada.");
        }
        Object[] values = {
            conta.getNomeCliente(),
            hashMD5IfNecessary(conta.getSenha()),
            conta.getSaldo(),
            conta.getNroConta()
        };
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            connection = daoFactory.getConnection();
            preparedStatement = DAOUtil.prepareStatement(connection, SQL_UPDATE, false, values);
            int affectedRows = preparedStatement.executeUpdate();
            if (affectedRows == 0) {
                throw new DAOException("Atualização de conta falhou, nenhuma linha foi afetada.");
            }
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }
    }

    public void save(Conta conta) throws DAOException {
        if (conta.getNroConta() == null) {
            create(conta);
        } else {
            update(conta);
        }
    }

    public void delete(Conta conta) throws DAOException {
        Object[] values = { conta.getNroConta() };

        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = daoFactory.getConnection();
            preparedStatement = DAOUtil.prepareStatement(connection, SQL_DELETE, false, values);
            int affectedRows = preparedStatement.executeUpdate();
            if (affectedRows == 0) {
                throw new DAOException("Remoção da conta falhou, nenhuma linha foi afetada.");
            } else {
                conta.setNroConta(null);
            }
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }
    }



    /**
     * Returns true if the given SQL query with the given values returns at least one row.
     * @param sql The SQL query to be executed in the database.
     * @param values The PreparedStatement values to be set.
     * @return True if the given SQL query with the given values returns at least one row.
     * @throws DAOException If something fails at database level.
     */
    private boolean exist(String sql, Object... values) throws DAOException {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        boolean exist = false;

        try {
            connection = daoFactory.getConnection();
            preparedStatement = DAOUtil.prepareStatement(connection, sql, false, values);
            resultSet = preparedStatement.executeQuery();
            exist = resultSet.next();
        } catch (SQLException e) {
            throw new DAOException(e);
        } finally {
            DAOUtil.close(resultSet);
            DAOUtil.close(preparedStatement);
            DAOUtil.close(connection);
        }

        return exist;
    }

    private static String hashMD5IfNecessary(String password) {
        return !"^[a-f0-9]{32}$".matches(password) ? DAOUtil.hashMD5(password) : password;
    }

    private static Conta mapConta(ResultSet resultSet) throws SQLException {
        return new Conta(
            Long.valueOf(resultSet.getLong("nro_conta")),
            resultSet.getString("nome_cliente"),
            resultSet.getString("senha"),
            resultSet.getFloat("saldo")
        );
    }

}

